package clasificador.vecinosproximos;

import java.util.*;

import metrica.DistanciaEuclidea;
import metrica.DistanciaVectorial;
import clasificador.ClasificadorEntrenado;
import datos.Atributo;
import datos.Dato;
import datos.IBuilder;
import datos.MuestraClasificada;
import datos.VectorizadorAtributos;

/**
 *
 * @author e235166
 */
public class ClasificadorVecinosProximos<Clase> extends ClasificadorEntrenado<Clase> {
	/* La distancia es la distancia basica a utilizar. El clasificador se encarga de renormalizarla por dentro. */
    private DistanciaVectorial distanciaOriginal;
    private DistanciaNormalizada distanciaNormalizada;
    private VectorizadorAtributos vectorizador;

    /* Numero de vecinos a considerar */
    private Integer k;

    /* Datos originales ya convertidos en vectores y renormalizados */
    private Map<Double[],Clase> elementos;

    public ClasificadorVecinosProximos(MuestraClasificada<Clase> datosEntrenamiento, Map<String, Object> parametros) {
        super(datosEntrenamiento, parametros);
    }

    @Override
    protected void inicializarParametros(Map<String, Object> parametros) {
        if(parametros.get("distancia") == null)
        {
            parametros.put("distancia", new DistanciaEuclidea());
        }
        if(parametros.get("k") == null)
        {
            throw new RuntimeException("Debe especificar el numero de vecinos a considerar en la clasificacion.");
        }

        this.k = (Integer)parametros.get("k");
        this.vectorizador = (VectorizadorAtributos)parametros.get("vectorizador");
        this.distanciaOriginal = (DistanciaVectorial)parametros.get("distancia");
    }

	/* Define una distancia entre datos como una distancia vectorial entre sus atributos numericos */
	/* */
	/* Solo renormaliza el segundo elemento comparado. Esto es asi por eficiencia, dado que es una clase privada, de forma que */
	/* siempre se pase como primer elemento el elemento previamente calculado (el que forma parte de los datos), que ya esta normalizado. */
	/* Asi se evita renormalizar los datos originales en cada ocasion */
	private static class DistanciaNormalizada extends DistanciaVectorial {
		private DistanciaVectorial distanciaBase;
		private Double[] factoresNormalizacion;

		private DistanciaNormalizada(DistanciaVectorial distanciaBase, Double[] factoresNormalizacion)
		{
			this.distanciaBase = distanciaBase;
			this.factoresNormalizacion = factoresNormalizacion;
		}

		@Override
		public double distanciaVectorial(Double[] a, Double[] b) {
			/* Esto ser�a lo razonable, por eficiencia se hace lo segundo */
			// return distanciaBase.distancia(factorizar(a), factorizar(b));
			return distanciaBase.distancia(a,b);
		}

		private Double[] factorizar(Double[] a)
		{
			Double[] resultado = new Double[a.length];

			for(int i = 0; i < a.length; i++)
			{
				resultado[i] = a[i]*factoresNormalizacion[i];
			}

			return resultado;
		}

		private static class Builder implements IBuilder<Double[],DistanciaNormalizada>
		{
			private Double[] media;
			private Double[] sumaCuadrados;
			private Double cuenta;
			private int dimension;
			private DistanciaVectorial distanciaBase;

			private Builder(int dimension, DistanciaVectorial distanciaBase)
			{
				this.dimension = dimension;
				this.distanciaBase = distanciaBase;
                this.media = new Double[dimension];
                this.sumaCuadrados = new Double[dimension];
			}

			@Override
			public void add(Double[] c) {
				cuenta++;
				for(int i = 0; i < dimension; i++)
				{
					Double delta = c[i] - media[i];
					media[i] = media[i] + delta/cuenta;
					sumaCuadrados[i] = sumaCuadrados[i] + delta * (c[i] - media[i]);
				}
			}

			@Override
			public DistanciaNormalizada finish() {
				Double[] factores = new Double[dimension];
				for(int i = 0; i < dimension; i++)
				{
					/* El factor de normalizacion es el inverso de la desviacion tipica */
					factores[i] = Math.sqrt((cuenta - 1) / sumaCuadrados[i]);
				}

				return new DistanciaNormalizada(distanciaBase,factores);
			}

			@Override
			public void reset() {
				cuenta = 0.0;
				for(int i = 0; i < dimension; i++)
				{
					media[i] = 0.0;
					sumaCuadrados[i] = 0.0;
				}
			}
		}
	}

	@Override
    protected void entrenar(MuestraClasificada<Clase> datosEntrenamiento) {
        System.out.println("INICIANDO ENTRENAMIENTO");
        System.out.flush();

		if(this.vectorizador == null)
		{
			List<Atributo<?>> atributosOrigen = datosEntrenamiento.getAtributos();
			Atributo<Double>[] atributos = new Atributo[datosEntrenamiento.getAtributos().size()-1];

			for(int i = 0, j = 0; i < datosEntrenamiento.getAtributos().size(); i++)
			{
				Atributo<?> atributo = atributosOrigen.get(i);
				if(!atributo.equals(datosEntrenamiento.getClase()))
				{
                    if (! Number.class.isAssignableFrom( atributo.getTipo() )) {
                        throw new RuntimeException("Non-numerical attribute in KNN classifier");
                    }
                    atributos[j] = (Atributo<Double>)atributo;
					j++;
				}
			}

			this.vectorizador = new VectorizadorAtributos(atributos);
		}

		/* Hacemos dos pasadas a los datos a cambio de evitar calculos extra durante la clasificacion. Es un compromiso que hemos considerado adecuado, pero depende del uso */
		/* No se puede hacer todo de una pasada porque para guardar los datos renormalizados hay que haber calculado las distribuciones previamente */

    	/* Guardar los datos, pero ya listos para hacer distancias (en formato de array de doubles y renormalizados) */
        this.elementos = new HashMap<Double[],Clase>(datosEntrenamiento.getDatos().size());

    	/* Calcular la renormalizacion de los atributos (construir distancia renormalizada) */
		DistanciaNormalizada.Builder builder = new DistanciaNormalizada.Builder(datosEntrenamiento.getAtributos().size()-1, this.distanciaOriginal);
		builder.reset();
		for(Dato dato: datosEntrenamiento.getDatos())
		{			
			System.out.println(dato);
            Double [] dd = vectorizador.adapt(dato);
			builder.add(vectorizador.adapt(dato));
            this.elementos.put(dd, dato.getAtributo(datosEntrenamiento.getClase()));
		}
		this.distanciaNormalizada = builder.finish();


		/*for(Dato dato: datosEntrenamiento.getDatos())
		{
			this.elementos.put(distanciaNormalizada.factorizar(vectorizador.adapt(dato)),dato.getAtributo(datosEntrenamiento.getClase()));
		}*/
        System.out.println("FINALIZADO ENTRENAMIENTO");
        System.out.flush();
    }

    private class ComparadorClasificacion implements Comparator<Double[]>
	{
		private Double[] dato;

		private ComparadorClasificacion(Double[] dato)
		{
			this.dato = dato;
		}

		@Override
		public int compare(Double[] o1, Double[] o2) {
			/* El mas cercano primero, de forma que siempre se reemplaza el ultimo */
			return new Double(distanciaNormalizada.distancia(o1,dato) - distanciaNormalizada.distancia(o2,dato)).intValue();
		}
	}

    @Override
    public Clase clasificar(Dato d) {
        Double [] dd = vectorizador.adapt(d);
        dd = this.distanciaNormalizada.factorizar(dd);
        //Comparator<Double[]> comparator = new ComparadorClasificacion(vectorizador.adapt(d));

        // Esto es demasiado lento
        //SortedMap<Double[],Clase> vecinosProximos = new TreeMap<Double[],Clase>(comparator);

        ArrayList<Double> distancias = new ArrayList<Double>();
        ArrayList<Clase> clases = new ArrayList<Clase>();

        for(Map.Entry<Double[],Clase> vecino: this.elementos.entrySet())
        {
            double distancia = this.distanciaNormalizada.distancia(vecino.getKey(), dd);

            if (distancias.size() < this.k) {
                distancias.add(distancia);
                clases.add(vecino.getValue());
            } else {
                double max = distancia;
                int indexMax = -1;
                for(int i = 0; i < distancias.size(); i++) {
                    if (distancias.get(i) > max) {
                        max = distancias.get(i);
                        indexMax = i;
                    }
                }
                if (indexMax != -1) {
                    distancias.set(indexMax, distancia);
                    clases.set(indexMax, vecino.getValue());
                }
            }
        	/* Primero metemos el nuevo elemento y luego, si nos sobran, quitamos el �ltimo. */
        	/* Esto es m�s eficiente que quedarnos con los k primeros al final, pues evitamos tener que ordenar entre elementos */
        	/* que, en cualquier caso, est�n muy lejos de nuestro dato */
        	/* vecinosProximos.put(vecino.getKey(), vecino.getValue());
        	if(vecinosProximos.size() > this.k)
        	{
        		vecinosProximos.remove(vecinosProximos.lastKey());
        	} */
        }

        /* Cogemos la clase m�s frecuente entre los vecinos pr�ximos */
        Map<Clase,Integer> frecuencias = new HashMap<Clase,Integer>();
        Clase claseFinal = null;

        for(Clase clase: clases)
        {
        	Integer frq = frecuencias.get(clase);
        	if(frq == null)
        	{
        		frq = 0;
        	}
        	frq++;
        	frecuencias.put(clase,frq);
        	if(claseFinal == null || frq > frecuencias.get(claseFinal))
        	{
        		claseFinal = clase;
        	}
        }

        return claseFinal;
    }
}
